"""
@File    :  multi_dhcp_client_thread.py
@Author  :  LeeSen
@Date    :  2024-08-05
@Version :  Python 3.7.4
"""

import threading
import time
from PyQt5.QtCore import QThread, pyqtSignal
from my_dhcp_client import DhcpState, DhcpEvent, DhcpMessageDTO, DhcpPackageType
from scapy_function import send_dhcp_pkt, send_arp


class MultiDhcpClientThread(QThread):
    pkt_signal = pyqtSignal(dict)

    def __init__(self,
                 nic,
                 message_sharing_queues,
                 dhcp_client_dict,
                 host_mac_to_ip_dict,
                 host_mac_to_timer_dict,
                 dhcp_transaction_id_set):
        super().__init__()
        self.stop_flag = False
        self.nic = nic
        self.message_sharing_queues = message_sharing_queues
        self.dhcp_client_dict = dhcp_client_dict
        self.host_mac_to_ip_dict = host_mac_to_ip_dict
        self.host_mac_to_timer_dict = host_mac_to_timer_dict
        self.dhcp_transaction_id_set = dhcp_transaction_id_set

    def run(self):
        res = {"code": "200", "message": "DHCP Client Thread will Start", "data": ""}
        self.pkt_signal.emit(res)
        while not self.stop_flag:

            if not self.message_sharing_queues.empty():

                message_dto = self.message_sharing_queues.get()
                if message_dto.host_mac in self.host_mac_to_timer_dict.keys():

                    if not (self.host_mac_to_timer_dict[message_dto.host_mac] is None):
                        self.host_mac_to_timer_dict[message_dto.host_mac].cancel()
                        self.host_mac_to_timer_dict[message_dto.host_mac] = None
                    res["code"] = "666"
                    res["message"] = "DHCP Client "
                    temp_state = self.dhcp_client_dict[message_dto.host_mac].current_state
                    dhcp_state_fsm_handle(self.dhcp_client_dict[message_dto.host_mac],
                                          message_dto.current_event,
                                          message_dto.pkt,
                                          self.message_sharing_queues,
                                          self.host_mac_to_timer_dict,
                                          self.nic,
                                          self.dhcp_transaction_id_set)
                    res["data"] = f"MAC:[{message_dto.host_mac}]" \
                                  f" | Current-State:[{temp_state}]" \
                                  f" | Current-Event:[{message_dto.current_event}]" \
                                  f" | Next-State:[{self.dhcp_client_dict[message_dto.host_mac].current_state}]"
                    self.pkt_signal.emit(res)

            for dhcp_client_item in self.dhcp_client_dict.values():
                if dhcp_client_item.current_state == DhcpState.INIT:
                    dhcp_client_item.discover_stage = True
                    dhcp_state_fsm_handle(dhcp_client_item,
                                          DhcpEvent.NULL,
                                          None,
                                          self.message_sharing_queues,
                                          self.host_mac_to_timer_dict,
                                          self.nic,
                                          self.dhcp_transaction_id_set)
                elif dhcp_client_item.current_state == DhcpState.BOUND:
                    host_ip = dhcp_client_item.ip
                    host_mac = dhcp_client_item.mac
                    self.host_mac_to_ip_dict[host_mac] = host_ip

                if self.stop_flag:
                    res = {"code": "200", "message": "DHCP Client Thread will Stop", "data": ""}
                    self.pkt_signal.emit(res)
                    break

    def stop(self):
        self.stop_flag = True


def timer_handle_callback(**kwargs):
    dhcp_message = DhcpMessageDTO(kwargs["host_mac"], None, DhcpEvent.TIMEOUT)
    kwargs["message_sharing_queues"].put(dhcp_message)


def timer_generate(interval, host_mac, timer_dict, message_queues):
    timer_dict[host_mac] = threading.Timer(interval,
                                           timer_handle_callback,
                                           kwargs={"host_mac": host_mac,
                                                   "message_sharing_queues": message_queues})
    timer_dict[host_mac].start()


def dhcp_state_fsm_handle(dhcp_client,
                          dhcp_event,
                          dhcp_server_message,
                          message_queues,
                          dhcp_timer_dict,
                          nic,
                          dhcp_transaction_id_set):

    if dhcp_client.current_state == DhcpState.INIT:
        dhcp_client.current_state = DhcpState.SELECTING
        send_dhcp_pkt(DhcpPackageType.DISCOVER.value, dhcp_client, nic)
        timer_generate(dhcp_client.default_time, dhcp_client.mac, dhcp_timer_dict, message_queues)
    elif dhcp_client.current_state == DhcpState.SELECTING and dhcp_event == DhcpEvent.OFFER_RECEIVED:
        dhcp_client.current_state = DhcpState.REQUESTING
        dhcp_client.request_ip = dhcp_server_message.client_ip
        dhcp_client.dhcp_server_id = dhcp_server_message.server_ip
        send_dhcp_pkt(DhcpPackageType.REQUEST.value, dhcp_client, nic)
        timer_generate(dhcp_client.default_time, dhcp_client.mac, dhcp_timer_dict, message_queues)
    elif dhcp_client.current_state == DhcpState.SELECTING and dhcp_event == DhcpEvent.TIMEOUT:
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.REQUESTING and dhcp_event == DhcpEvent.ACK:
        dhcp_client.current_state = DhcpState.WAITING
        dhcp_client.request_ip = dhcp_server_message.client_ip
        dhcp_client.dhcp_server_id = dhcp_server_message.server_ip
        dhcp_client.ip = dhcp_server_message.client_ip
        dhcp_client.server_mac = dhcp_server_message.server_mac
        dhcp_client.serve_ip = dhcp_server_message.server_ip
        dhcp_client.expired = dhcp_server_message.lease_time
        dhcp_client.time_1 = int(dhcp_client.expired * 4 / 8) - dhcp_client.arp_time
        dhcp_client.time_2 = dhcp_client.expired * 3 / 8
        dhcp_client.time_3 = dhcp_client.expired * 1 / 8
        send_arp(dhcp_client, nic)
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
        timer_generate(dhcp_client.arp_time, dhcp_client.mac, dhcp_timer_dict, message_queues)
    elif dhcp_client.current_state == DhcpState.REQUESTING and dhcp_event == DhcpEvent.NACK:
        send_dhcp_pkt(DhcpPackageType.DECLINE.value, dhcp_client, nic)
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.REQUESTING and dhcp_event == DhcpEvent.TIMEOUT:
        send_dhcp_pkt(DhcpPackageType.DECLINE.value, dhcp_client, nic)
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.WAITING and dhcp_event == DhcpEvent.TIMEOUT:
        dhcp_client.current_state = DhcpState.BOUND
        dhcp_client.discover_stage = False
        dhcp_client.start_time = int(time.time())
        timer_generate(dhcp_client.time_1, dhcp_client.mac, dhcp_timer_dict, message_queues)
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.WAITING and dhcp_event == DhcpEvent.ARP_RECEIVED:
        send_dhcp_pkt(DhcpPackageType.DECLINE.value, dhcp_client, nic)
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.BOUND and dhcp_event == DhcpEvent.TIMEOUT:
        dhcp_client.current_state = DhcpState.RENEWING
        send_dhcp_pkt(DhcpPackageType.REQUEST.value, dhcp_client, nic)
        timer_generate(dhcp_client.time_2, dhcp_client.mac, dhcp_timer_dict, message_queues)
    elif dhcp_client.current_state == DhcpState.RENEWING and dhcp_event == DhcpEvent.ACK:
        dhcp_client.current_state = DhcpState.BOUND
        dhcp_client.start_time = int(time.time())
        timer_generate(dhcp_client.time_1, dhcp_client.mac, dhcp_timer_dict, message_queues)
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.RENEWING and dhcp_event == DhcpEvent.TIMEOUT:
        dhcp_client.current_state = DhcpState.REBINDING
        dhcp_client.server_mac = "ff:ff:ff:ff:ff:ff"
        dhcp_client.serve_ip = "255.255.255.255"
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
        send_dhcp_pkt(DhcpPackageType.REQUEST.value, dhcp_client, nic)
        timer_generate(dhcp_client.time_3, dhcp_client.mac, dhcp_timer_dict, message_queues)
    elif dhcp_client.current_state == DhcpState.RENEWING and dhcp_event == DhcpEvent.NACK:
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.REBINDING and dhcp_event == DhcpEvent.ACK:
        dhcp_client.current_state = DhcpState.BOUND
        dhcp_client.request_ip = dhcp_server_message.client_ip
        dhcp_client.dhcp_server_id = dhcp_server_message.server_ip
        dhcp_client.ip = dhcp_server_message.client_ip
        dhcp_client.server_mac = dhcp_server_message.server_mac
        dhcp_client.serve_ip = dhcp_server_message.server_ip
        dhcp_client.expired = dhcp_server_message.lease_time
        dhcp_client.time_1 = int(dhcp_client.expired * 4 / 8) - dhcp_client.arp_time
        dhcp_client.time_2 = dhcp_client.expired * 3 / 8
        dhcp_client.time_3 = dhcp_client.expired * 1 / 8
        dhcp_client.start_time = int(time.time())
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
        timer_generate(dhcp_client.time_1, dhcp_client.mac, dhcp_timer_dict, message_queues)

    elif dhcp_client.current_state == DhcpState.REBINDING and dhcp_event == DhcpEvent.TIMEOUT:
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    elif dhcp_client.current_state == DhcpState.REBINDING and dhcp_event == DhcpEvent.NACK:
        dhcp_client.current_state = DhcpState.INIT
        dhcp_client.reset_dhcp_parameters()
        dhcp_client.reset_transaction_id(dhcp_transaction_id_set)
    else:
        pass
